package org.rockyang.blockj.base.crypto;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.rockyang.blockj.base.exceptions.CipherException;
import org.rockyang.blockj.base.utils.Numeric;

import java.io.File;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

import static org.rockyang.blockj.base.crypto.Hash.sha256;
import static org.rockyang.blockj.base.crypto.Keys.ADDRESS_LENGTH_IN_HEX;
import static org.rockyang.blockj.base.crypto.Keys.PRIVATE_KEY_LENGTH_IN_HEX;

/**
 * Utility functions for working with Wallet files.
 */
public class WalletUtils {

	private static final ObjectMapper objectMapper = new ObjectMapper();
	private static final SecureRandom secureRandom = SecureRandomUtils.secureRandom();

	static {
		objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
		objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
	}

	public static String generateFullNewWalletFile(String password, File destinationDirectory)
			throws NoSuchAlgorithmException, NoSuchProviderException,
			InvalidAlgorithmParameterException, CipherException, IOException
	{

		return generateNewWalletFile(password, destinationDirectory, true);
	}

	public static String generateLightNewWalletFile(String password, File destinationDirectory)
			throws NoSuchAlgorithmException, NoSuchProviderException,
			InvalidAlgorithmParameterException, CipherException, IOException
	{

		return generateNewWalletFile(password, destinationDirectory, false);
	}

	public static String generateNewWalletFile(String password, File destinationDirectory)
			throws CipherException, InvalidAlgorithmParameterException,
			NoSuchAlgorithmException, NoSuchProviderException, IOException
	{
		return generateFullNewWalletFile(password, destinationDirectory);
	}

	public static String generateNewWalletFile(
			String password, File destinationDirectory, boolean useFullScrypt)
			throws CipherException, IOException, InvalidAlgorithmParameterException,
			NoSuchAlgorithmException, NoSuchProviderException
	{

		ECKeyPair ecKeyPair = Keys.createEcKeyPair();
		return generateWalletFile(password, ecKeyPair, destinationDirectory, useFullScrypt);
	}

	public static String generateWalletFile(
			String password, ECKeyPair ecKeyPair, File destinationDirectory, boolean useFullScrypt)
			throws CipherException, IOException
	{

		WalletFile walletFile;
		if (useFullScrypt) {
			walletFile = Wallet.createStandard(password, ecKeyPair);
		} else {
			walletFile = Wallet.createLight(password, ecKeyPair);
		}

		String fileName = getWalletFileName(walletFile);
		File destination = new File(destinationDirectory, fileName);

		objectMapper.writeValue(destination, walletFile);

		return fileName;
	}

	/**
	 * Generates a BIP-39 compatible Ethereum wallet. The private key for the wallet can
	 * be calculated using following algorithm:
	 * <pre>
	 *     Key = SHA-256(BIP_39_SEED(mnemonic, password))
	 * </pre>
	 *
	 * @param password             Will be used for both wallet encryption and passphrase for BIP-39 seed
	 * @param destinationDirectory The directory containing the wallet
	 * @return A BIP-39 compatible Ethereum wallet
	 * @throws CipherException if the underlying cipher is not available
	 * @throws IOException     if the destination cannot be written to
	 */
	public static Bip39Wallet generateBip39Wallet(String password, File destinationDirectory)
			throws Exception
	{
		byte[] initialEntropy = new byte[16];
		secureRandom.nextBytes(initialEntropy);

		String mnemonic = MnemonicUtils.generateMnemonic(initialEntropy);
		byte[] seed = MnemonicUtils.generateSeed(mnemonic, password);
		ECKeyPair privateKey = ECKeyPair.create(sha256(seed));

		String walletFile = generateWalletFile(password, privateKey, destinationDirectory, false);

		return new Bip39Wallet(privateKey, walletFile, mnemonic);
	}

	public static Bip39Wallet generateBip39Wallet(String password)
			throws Exception
	{
		byte[] initialEntropy = new byte[16];
		secureRandom.nextBytes(initialEntropy);

		String mnemonic = MnemonicUtils.generateMnemonic(initialEntropy);
		byte[] seed = MnemonicUtils.generateSeed(mnemonic, password);
		ECKeyPair privateKey = ECKeyPair.create(sha256(seed));


		return new Bip39Wallet(privateKey, null, mnemonic);
	}

	public static Bip39Wallet generateBip39Wallet()
			throws Exception
	{
		return generateBip39Wallet(null);
	}

	public static Credentials loadCredentials(String password, String source)
			throws Exception
	{
		return loadCredentials(password, new File(source));
	}

	public static Credentials loadCredentials(String password, File source)
			throws Exception
	{
		WalletFile walletFile = objectMapper.readValue(source, WalletFile.class);
		return Credentials.create(Wallet.decrypt(password, walletFile));
	}

	public static Credentials loadBip39Credentials(String password, String mnemonic) throws Exception
	{
		byte[] seed = MnemonicUtils.generateSeed(mnemonic, password);
		return Credentials.create(ECKeyPair.create(sha256(seed)));
	}

	public static Credentials loadBip39Credentials(String mnemonic) throws Exception
	{
		return loadBip39Credentials(null, mnemonic);
	}

	private static String getWalletFileName(WalletFile walletFile)
	{
		DateTimeFormatter format = DateTimeFormatter.ofPattern(
				"'UTC--'yyyy-MM-dd'T'HH-mm-ss.nVV'--'");
		ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);

		return now.format(format) + walletFile.getAddress() + ".json";
	}

	public static String getDefaultKeyDirectory()
	{
		return getDefaultKeyDirectory(System.getProperty("os.name"));
	}

	static String getDefaultKeyDirectory(String osName1)
	{
		String osName = osName1.toLowerCase();

		if (osName.startsWith("mac")) {
			return String.format(
					"%s%sLibrary%sEthereum", System.getProperty("user.home"), File.separator,
					File.separator);
		} else if (osName.startsWith("win")) {
			return String.format("%s%sEthereum", System.getenv("APPDATA"), File.separator);
		} else {
			return String.format("%s%s.ethereum", System.getProperty("user.home"), File.separator);
		}
	}

	public static String getTestnetKeyDirectory()
	{
		return String.format(
				"%s%stestnet%skeystore", getDefaultKeyDirectory(), File.separator, File.separator);
	}

	public static String getMainnetKeyDirectory()
	{
		return String.format("%s%skeystore", getDefaultKeyDirectory(), File.separator);
	}

	public static boolean isValidPrivateKey(String privateKey)
	{
		String cleanPrivateKey = Numeric.cleanHexPrefix(privateKey);
		return cleanPrivateKey.length() == PRIVATE_KEY_LENGTH_IN_HEX;
	}

	public static boolean isValidAddress(String input)
	{
		String cleanInput = Numeric.cleanHexPrefix(input);

		try {
			Numeric.toBigIntNoPrefix(cleanInput);
		} catch (NumberFormatException e) {
			return false;
		}

		return cleanInput.length() == ADDRESS_LENGTH_IN_HEX;
	}
}
